基本语法

fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1,param2,...])
fun.bind(thisArg, param1, param2, ...)

返回值

call()apply() 返回函数应该返回的值,bind() 返回一个经过硬绑定的新函数。

参数介绍

第一个参数为 thisArg,其取值有以下几种情况:

  • 不传/ 传null/ 传undefined:非严格模式下,this 指向 window 对象;严格模式下指向 undefined;

  • 传递基本类型:this 指向其对应的包装对象,如 String、Number、Boolean

  • 传递一个对象:函数中的 this 指向这个对象

第二个参数有以下几种情况:

  • 不传/ 传null/ 传undefined:表示不需要传入任何参数
  • call()bind() 的第二个参数都是参数列表,而 apply() 则是参数数组(或者类数组)—— 尽管如此,在这些参数传递给调用函数时,仍然是以参数列表的形式传递的(这一点很重要)。

执行

call()apply() 一经调用则立即执行函数,而 bind() 则只是完成了函数的 this 绑定。因为函数不会立刻执行,所以适合在事件绑定函数中使用 bind() ,这样既完成了绑定,也确保了仅当事件触发时才执行函数。

应用场景

这篇文章说过,call()apply()bind() 都可以改变 this 的指向,什么时候需要改变 this 的指向呢?大部分时候其实是为了借用方法,即在对象上调用其自身不具备的方法。看一下下面的例子:

1. 方法借用:判断数据类型

利用 Object.prototype.toString.call() 可以准确地判断数据类型,如:

var a = "abc";
var b = [1,2,3];
Object.prototype.toString.call(a) == "[object String]" //true
Object.prototype.toString.call(b) == "[object Array]"  //true

原理就是:在任何值上调用 Object 原生的 toString() 方法,都会返回一个格式为 [object NativeconstructorName] 的字符串。据此可以准确判断任何值的数据类型。
既然 Array 和 Function 都继承了 Object 的该方法,为什么不直接在它们身上调用?这是因为 toString() 被重写过了,不是原生方法,因此这里改为调用 Object 的该方法,并将 this 绑定给对应的值。

2. 方法借用:类数组使用数组方法

例如 arguments 是类数组,并不具备数组的 forEach() 方法,那么我们可以通过 call() 调用数组的该方法,同时将方法里面的 this 绑定到 arguments 上:

Array.prototype.forEach.call(arguments,function(item){
     console.log(item);
});

类数组借用数组的方法,还可以将类数组转化为数组:

Array.prototype.slice.call(arguments)   // slice 本身会返回一个数组

当然还有其它转化方法:

[].slice.call(arguments)
[...arguments]
Array.from(arguments)

3. 模拟浅拷贝

模拟浅拷贝的过程中,需要剔除原型链上的属性,考虑到源对象可能基于 Object.create() 创建,而这样的对象是没有 hasOwnProperty() 方法的,因此我们不在源对象身上直接调用该方法,而是通过 Object.prototype.hasOwnProperty.call() 的方式去调用,因为 Object 一定是有这个方法的,我们可以借用一下。

if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
    to[nextKey] = nextSource[nextKey];
}

4. 增强子类实例

JavaScript 的几种继承方式中,有一种就是借用构造函数:
假设有子构造函数 Son 和父构造函数 Parent。对于 Son 而言,其内部的 this 将指向稍后实例化的对象,利用这一点,我们在 Son 的内部通过 call() 或者 apply() 调用 Parent,同时传参 this,这样就可以增强子类实例。

5. 求数组的最值

apply() 可用于展开数组,即传进去的第二个参数是一个参数数组,但实际执行的时候会被转化为一个参数列表。利用这一点,我们可以求一个数组的最大值 —— 虽然 Math 对象有 max() 方法,但该方法只接受参数列表。那么这时候,我们可以将该方法以 apply() 的方式去调用,从而展开数组:

var arr = [2,3,1,5,4];
 
Math.max.apply(null,arr);// 5

6. 延迟执行函数的 this 绑定

bindcall / apply的一个重要区别就在于,它只是对原函数提前做了一个 this 绑定,并没有马上去执行函数。因此对于那些延迟执行但又容易发生 this 丢失的函数(比如定时器的回调函数),我们可以在声明的时候先通过 bind 绑定一个 this。比如:

var value = 1
const obj = {
    value : 2,
    fn(){
        setTimeout(function(){
        	console.log(this.value)    
        },1000)
    }
} 
obj.fn()    // 1

像这样直接调用会发生 this 丢失的问题,因为 setTimeout 本质上还是通过 window 调用的,所以 this 会指向 window。而这个回调函数我们又不想马上执行,只是想它在执行的时候绑定一个正确的 this,因此我们可以使用 bind:

var value = 1
const obj = {
    value : 2,
    fn(){
        setTimeout(function(){
        	console.log(this.value)    
        }.bind(this),1000)
    }
} 
obj.fn()    // 2

7. 实现柯里化包装函数

比如说现在有一个 add 函数:

function add(a,b){
    return a + b 
}
add(1,2)  // 3

如果想要让 add 函数做到类似 add(1)(2) 这样的分批次接收参数,且最终执行结果是一样的,应该怎么办呢?可以将 add 改写如下:

function add(a){
    return function(b){
        return a + b 
    }
}

但是参数是灵活的,我们更希望实现一个通用的柯里化包装函数,传进去的函数经过包装之后,会返回一个原函数的柯里化版本。这里就可以使用 bind 来实现了:

function curry(fn){
    const len = fn.length
    return function fnCurried(){
        if(arguments.length < len){
            return fnCurried.bind(null,...arguments)
        } else {
            return fn(...arguments)
        }
    }
}
const curryAdd = curry(add)
curryAdd(1,2)    // 3
curryAdd(1)(2)   // 3

几个要点:

  • 将目标函数 fn 传给包装函数 curry 之后,会返回一个柯里化版本的函数 fnCurried。对于 fnCurried,我们在调用的时候,可以选择一次性传完所有参数,也可以选择分批次传参数,那么如何判断呢?如果是分批次传参的话,传的参数个数 arguments.length 一定会小于 fn 实际应该接受的参数 fn.length,反之则是一次性传所有参数
  • 如果是一次性传所有参数,那就比较简单了,直接返回原函数 fn 的调用结果就行(注意要展开 arguments)
  • 如果是分批次传参数,那么就重复返回相同的函数以供下次进行同样的调用。这里我们使用 bind 并不是为了修改 this 指向,所以传一个 null 就行,我们只是为了利用 bind 可以分批次传参这个特点来收集每次调用得到的参数而已。

参考:
https://www.cnblogs.com/onepixel/p/6034307.html
https://juejin.im/post/5d469e0851882544b85c32ef